package sqlx

import (
	"errors"
	"fmt"
	"os"
	"path"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	_ "github.com/mattn/go-sqlite3"

	"clean/infra/config"
	"clean/infra/repo"
	"clean/usecase"

	sql_sqlx "clean/adapter/repo/sql/sqlx"
)

// see http://jmoiron.github.io/sqlx/

const sqlite3Memory = ":memory:"

func init() {
	repo.Register("sql_sqlx", &repoDriver{})
}

type repoDriver struct {
}

func (repoDriver) Create(cfg config.Config) (*usecase.Repo, error) {
	if cfg.Repo.Clear {
		err := mayDropDb(cfg)
		if err != nil {
			return nil, err
		}
	}
	err := mayCreateDb(cfg)
	if err != nil {
		return nil, err
	}
	dataSourceName := getDataSourceName(cfg)
	db, err := sqlx.Open(cfg.Repo.Sql.Sqlx.Driver, dataSourceName)
	if err != nil {
		return nil, err
	}
	db.SetMaxIdleConns(cfg.Repo.Sql.Sqlx.Mysql.MaxIdle)
	db.SetMaxOpenConns(cfg.Repo.Sql.Sqlx.Mysql.MaxOpen)
	db.SetConnMaxLifetime(time.Hour)
	err = db.Ping()
	if err != nil {
		_ = db.Close()
		return nil, err
	}
	repo, err := sql_sqlx.InitRepo(db)
	if err != nil {
		_ = db.Close()
		return nil, err
	}
	if db.DriverName() == "mysql" {
		// 通过mysql-ping来维护长连接
		go alive(db)
	}
	return repo, nil
}

// 如果是mysql删除db
// 如果是sqlite删除文件
func mayDropDb(cfg config.Config) error {
	if cfg.Repo.Sql.Sqlx.Driver == "sqlite3" {
		if cfg.Repo.Sql.Sqlx.Sqlite3.Db == sqlite3Memory {
			return nil
		}
		dataSource := path.Join(cfg.Repo.Sql.Sqlx.Sqlite3.Dir, cfg.Repo.Sql.Sqlx.Sqlite3.Db)
		f, err := os.Open(dataSource)
		if err != nil {
			var pathErr *os.PathError
			if errors.As(err, &pathErr) {
				return nil
			}
			return err
		}
		_ = f.Close()
		return os.Remove(dataSource)
	} else if cfg.Repo.Sql.Sqlx.Driver == "mysql" {
		db, err := sqlx.Open(cfg.Repo.Sql.Sqlx.Driver, dialect("", cfg))
		if err != nil {
			return fmt.Errorf("mayDropDb: %w", err)
		}
		defer db.Close()
		_, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %v", cfg.Repo.Sql.Sqlx.Mysql.Db))
		if err != nil {
			return fmt.Errorf("mayDropDb: %w", err)
		}
	}
	return nil
}

// 如果是mysql并且db不存在需要先创建db
// 如果是sqlite并且目录不存在则需要创建目录
func mayCreateDb(cfg config.Config) error {
	if cfg.Repo.Sql.Sqlx.Driver == "sqlite3" {
		if cfg.Repo.Sql.Sqlx.Sqlite3.Db == sqlite3Memory {
			return nil
		}
		if cfg.Repo.Sql.Sqlx.Sqlite3.Dir == "" {
			return nil
		}
		f, err := os.Open(cfg.Repo.Sql.Sqlx.Sqlite3.Dir)
		if err != nil {
			var pathErr *os.PathError
			if errors.As(err, &pathErr) {
				return os.MkdirAll(cfg.Repo.Sql.Sqlx.Sqlite3.Dir, os.ModePerm)
			} else {
				return err
			}
		} else {
			info, err := f.Stat()
			if err != nil {
				return err
			}
			if info.IsDir() {
				return nil
			}
			return fmt.Errorf("sqlite3.dir = %v exist but is not dir", cfg.Repo.Sql.Sqlx.Sqlite3.Dir)
		}
	} else if cfg.Repo.Sql.Sqlx.Driver == "mysql" {
		db, err := sqlx.Open(cfg.Repo.Sql.Sqlx.Driver, dialect("", cfg))
		if err != nil {
			return fmt.Errorf("mayCreateDb: %w", err)
		}
		defer db.Close()
		_, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %v /*!40100 COLLATE 'utf8_general_ci' */", cfg.Repo.Sql.Sqlx.Mysql.Db))
		if err != nil {
			return fmt.Errorf("mayCreateDb: %w", err)
		}
	}
	return nil
}

func getDataSourceName(cfg config.Config) string {
	var dataSourceName string
	switch cfg.Repo.Sql.Sqlx.Driver {
	case "mysql":
		dataSourceName = dialect(cfg.Repo.Sql.Sqlx.Mysql.Db, cfg)
	case "sqlite3":
		dataSourceName = cfg.Repo.Sql.Sqlx.Sqlite3.Db
		if dataSourceName != sqlite3Memory {
			dataSourceName = path.Join(cfg.Repo.Sql.Sqlx.Sqlite3.Dir, cfg.Repo.Sql.Sqlx.Sqlite3.Db)
		}
	default:
		panic("unknown sql driver")
	}
	return dataSourceName
}

func dialect(dbName string, cfg config.Config) string {
	addr := fmt.Sprintf("tcp(%s:%v)", cfg.Repo.Sql.Sqlx.Mysql.Host, cfg.Repo.Sql.Sqlx.Mysql.Port)
	// remove &strict=true  https://github.com/go-sql-driver/mysql/wiki/strict-mode
	sqlDialect := fmt.Sprintf("%v:%v@%v/%v?charset=utf8&parseTime=True&loc=Local&timeout=%vs",
		cfg.Repo.Sql.Sqlx.Mysql.User, cfg.Repo.Sql.Sqlx.Mysql.Password, addr, dbName, cfg.Repo.Sql.Sqlx.Mysql.Timeout)
	return sqlDialect
}

func alive(db *sqlx.DB) {
	var count int
	for {
		err := db.Ping()
		if err != nil {
			count++
			time.Sleep(time.Second)
			continue
		} else {
			if count > 0 {

			}
			count = 0
		}
		time.Sleep(10 * time.Second)
	}
}
